/*
 * Copyright 2004-2009 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package susano.presen.controller;

import java.io.IOException;
import java.net.URL;
import java.util.Enumeration;
import java.util.Locale;
import java.util.TimeZone;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.lang.StringUtils;

import susano.common.exception.SusanoRuntimeException;
import susano.common.util.LocaleUtil;
import susano.presen.threadlocal.LocaleLocal;
import susano.presen.threadlocal.RequestLocal;
import susano.presen.threadlocal.ResponseLocal;
import susano.presen.threadlocal.ServletContextLocal;
import susano.presen.threadlocal.TimeZoneLocal;
import susano.presen.util.ApplicationMessage;
import susano.presen.util.PresenConstants;
import susano.presen.util.RequestUtil;
import susano.presen.validator.Errors;

/**
 * Controller Filter for susano-presen
 *
 * @author tanaka.akira.2006
 * @version $Id$
 */
public class Controller implements Filter {
	private static final Logger logger = Logger.getLogger(Controller.class
			.getName());
	protected String charset;
	protected String bundleName;
	protected Locale defaultLocale;
	protected TimeZone defaultTimeZone;
	protected ServletContext servletContext;
	protected boolean servletContextSet = false;
	protected String rootPackageName;

	public Controller() {
	}

	public void init(FilterConfig config) throws ServletException {
		checkDuplicateClasses();
		initServletContext(config);
		initCharset();
		initBundleName();
		initDefaultLocale();
		initDefaultTimeZone();
		initRootPackageName();
	}

	protected void checkDuplicateClasses() throws IllegalStateException {
		try {
			Enumeration<URL> resources = Thread.currentThread()
					.getContextClassLoader().getResources(
							getClass().getName().replace('.', '/') + ".class");
			int count = 0;
			while (resources.hasMoreElements()) {
				resources.nextElement();
				count++;
			}
			if (count > 1) {
				throw new IllegalStateException(
						"susano-xxx.jar files are duplicate in the classpath.");
			}
		} catch (IOException e) {
			throw new SusanoRuntimeException(e);
		}
	}

	/**
	 * Initializes the root package name.
	 */
	protected void initRootPackageName() {
		rootPackageName = servletContext
				.getInitParameter(PresenConstants.ROOT_PACKAGE_KEY);
		if (StringUtils.isEmpty(rootPackageName)) {
			throw new IllegalStateException("The context-param("
					+ PresenConstants.ROOT_PACKAGE_KEY
					+ ") is not found in web.xml.");
		}
	}

	/**
	 * Initializes the servlet context.
	 *
	 * @param config
	 *            the filter configuration.
	 */
	protected void initServletContext(FilterConfig config) {
		servletContext = config.getServletContext();
		if (ServletContextLocal.get() == null) {
			ServletContextLocal.set(servletContext);
			servletContextSet = true;
		} else {
			servletContext = ServletContextLocal.get();
		}
	}

	/**
	 * Initializes the character set.
	 */
	protected void initCharset() {
		charset = servletContext
				.getInitParameter(PresenConstants.REQUEST_CHARSET_KEY);
		if (charset == null) {
			charset = PresenConstants.DEFAULT_REQUEST_CHARSET;
		}
	}

	/**
	 * Initializes the default time zone.
	 */
	protected void initBundleName() {
		bundleName = servletContext
				.getInitParameter(PresenConstants.LOCALIZATION_CONTEXT_KEY);
		if (bundleName == null) {
			bundleName = PresenConstants.DEFAULT_LOCALIZATION_CONTEXT;
		}
	}

	/**
	 * Initializes the default locale.
	 */
	protected void initDefaultLocale() {
		defaultLocale = LocaleUtil.parse(servletContext
				.getInitParameter(PresenConstants.LOCALE_KEY));
	}

	/**
	 * Initializes the default time zone.
	 */
	protected void initDefaultTimeZone() {
		String s = servletContext
				.getInitParameter(PresenConstants.TIME_ZONE_KEY);
		if (s != null) {
			defaultTimeZone = TimeZone.getTimeZone(s);
		}
	}

	public void destroy() {
		if (servletContextSet) {
			ServletContextLocal.set(null);
		}
	}

	public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {
		doFilter((HttpServletRequest) request, (HttpServletResponse) response,
				chain);
	}

	/**
	 * Executes filtering process.
	 *
	 * @param request
	 *            the request
	 * @param response
	 *            the response
	 * @param chain
	 *            the filter chain
	 * @throws IOException
	 *             if {@link IOException} is encountered
	 * @throws ServletException
	 *             if {@link ServletException} is encountered
	 */
	protected void doFilter(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		String path = RequestUtil.getPath(request);
		String ext = RequestUtil.getExtension(path);
		if (ext == null) {
			HttpServletRequest previousRequest = RequestLocal.get();
			RequestLocal.set(request);
			HttpServletResponse previousResponse = ResponseLocal.get();
			ResponseLocal.set(response);
			Locale previousLocale = LocaleLocal.get();
			LocaleLocal.set(processLocale(request));
			TimeZone previousTimeZone = TimeZoneLocal.get();
			TimeZoneLocal.set(processTimeZone(request));
			ApplicationMessage.setBundle(bundleName, LocaleLocal.get());
			try {
				if (ext == null) {
					doFilterInternal(request, response, chain);
				} else {
					chain.doFilter(request, response);
				}
			} finally {
				ApplicationMessage.clearBundle();
				TimeZoneLocal.set(previousTimeZone);
				LocaleLocal.set(previousLocale);
				ResponseLocal.set(previousResponse);
				RequestLocal.set(previousRequest);
			}
		} else {
			chain.doFilter(request, response);
		}
	}

	/**
	 * Executes internal filtering process.
	 *
	 * @param request
	 *            the request
	 * @param response
	 *            the response
	 * @param chain
	 *            the filter chain
	 * @throws IOException
	 *             if {@link IOException} is encountered
	 * @throws ServletException
	 *             if {@link ServletException} is encountered
	 */
	protected void doFilterInternal(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		if (request.getCharacterEncoding() == null) {
			request.setCharacterEncoding(charset);
		}
		String path = RequestUtil.getPath(request);
		Action action = getAction(request, response, path);
		if (action == null) {
			chain.doFilter(request, response);
		} else {
			processAction(request, response, action);
		}
	}

	/**
	 * Processes the current locale.
	 *
	 * @param request
	 *            the request
	 * @return the current locale
	 */
	protected Locale processLocale(HttpServletRequest request) {
		Locale locale = null;
		HttpSession session = request.getSession(false);
		if (session != null) {
			Object o = session.getAttribute(PresenConstants.LOCALE_KEY);
			if (o instanceof String) {
				locale = LocaleUtil.parse((String) o);
			} else if (o instanceof Locale) {
				locale = (Locale) o;
			}
		}
		if (locale == null) {
			locale = defaultLocale;
		}
		if (locale == null) {
			locale = request.getLocale();
			if (locale == null) {
				locale = Locale.getDefault();
			}
		}
		return locale;
	}

	/**
	 * Processes the current time zone.
	 *
	 * @param request
	 *            the request
	 * @return the current time zone
	 */
	protected TimeZone processTimeZone(HttpServletRequest request) {
		TimeZone timeZone = null;
		HttpSession session = request.getSession(false);
		if (session != null) {
			Object o = session.getAttribute(PresenConstants.TIME_ZONE_KEY);
			if (o instanceof String) {
				timeZone = TimeZone.getTimeZone((String) o);
			} else if (o instanceof TimeZone) {
				timeZone = (TimeZone) o;
			}
		}
		if (timeZone == null) {
			timeZone = defaultTimeZone;
		}
		if (timeZone == null) {
			timeZone = TimeZone.getDefault();
		}
		return timeZone;
	}

	protected Action getAction(HttpServletRequest request,
			HttpServletResponse response, String path) {
		if (path.indexOf('.') >= 0) {
			return null;
		}
		Action action = createAction(path);
		if (action == null) {
			return null;
		}
		request.setAttribute(PresenConstants.ACTION_KEY, action);
		action.application = servletContext;
		action.request = request;
		action.response = response;
		int pos = path.lastIndexOf('/');
		action.basePath = path.substring(0, pos + 1);

		// Errorsをセット
		Errors errors = (Errors) request
				.getAttribute(PresenConstants.ERRORS_KEY);
		if (errors == null) {
			errors = new Errors();
			request.setAttribute(PresenConstants.ERRORS_KEY, errors);
		}
		action.errors = errors;
		return action;

	}

	/**
	 * Creates a new action specified by the path.
	 *
	 * @param path
	 *            the path
	 * @return a new action
	 * @throws IllegalStateException
	 *             if the action does not extend "org.slim3.action.Controller"
	 */
	protected Action createAction(String path) throws IllegalStateException {
		String className = toActionClassName(path);
		Class<?> clazz = null;
		ClassLoader loader = Thread.currentThread().getContextClassLoader();
		try {
			clazz = Class.forName(className, true, loader);
		} catch (Throwable t) {
			if (logger.isLoggable(Level.FINE)) {
				logger.log(Level.FINE, t.getMessage(), t);
			}
			return null;
		}
		if (!Action.class.isAssignableFrom(clazz)) {
			throw new IllegalStateException("The action(" + className
					+ ") must extend \"" + Action.class.getName() + "\".");
		}
		return newInstance(clazz);
	}

	/**
	 * Converts the path to the action class name.
	 *
	 * @param path
	 *            the path
	 * @return the action class name
	 * @throws IllegalStateException
	 *             if the system property(slim3.actionPackage) is not found
	 */
	protected String toActionClassName(String path)
			throws IllegalStateException {
		String className = rootPackageName + "."
				+ PresenConstants.DEFAULT_ACTION_PACKAGE
				+ path.replace('/', '.');
		if (className.endsWith(".")) {
			className += PresenConstants.INDEX_ACTION;
		} else {
			int pos = className.lastIndexOf('.');
			className = className.substring(0, pos + 1)
					+ capitalize(className.substring(pos + 1))
					+ PresenConstants.ACTION_SUFFIX;
		}
		return className;
	}

	/**
	 * Processes the action.
	 *
	 * @param request
	 *            the request
	 * @param response
	 *            the response
	 * @param action
	 * @throws IOException
	 *             if {@link IOException} has occurred
	 * @throws ServletException
	 *             if {@link ServletException} has occurred
	 */
	protected void processAction(HttpServletRequest request,
			HttpServletResponse response, Action action) throws IOException,
			ServletException {
		RequestHandler requestHandler = createRequestHandler(request);
		requestHandler.handle();
		Navigation navigation = action.doExecute();
		handleNavigation(request, response, action, navigation);
	}

	/**
	 * Creates a new request handlers.
	 *
	 * @param request
	 *            the request
	 * @return a new request handler
	 *
	 */
	protected RequestHandler createRequestHandler(HttpServletRequest request) {
		// if (FileUpload.isMultipartContent(request)) {
		// return new MultipartRequestHandler(request);
		// }
		return new RequestHandler(request);
	}

	/**
	 * Handles the navigation.
	 *
	 * @param request
	 *            the request
	 * @param response
	 *            the response
	 * @param action
	 *            the action
	 * @param navigation
	 *            the navigation
	 * @throws IOException
	 *             if {@link IOException} has occurred
	 * @throws ServletException
	 *             if {@link ServletException} has occurred
	 */
	protected void handleNavigation(HttpServletRequest request,
			HttpServletResponse response, Action action, Navigation navigation)
			throws IOException, ServletException {
		if (navigation == null) {
			return;
		}
		if (navigation.isRedirect()) {
			doRedirect(request, response, action, navigation.getPath());
		} else {
			doForward(request, response, action, navigation.getPath());
		}
	}

	/**
	 * Do a redirect to the path.
	 *
	 * @param request
	 *            the request
	 * @param response
	 *            the response
	 * @param action
	 *            the action
	 * @param path
	 *            the path
	 * @throws IOException
	 *             if {@link IOException} has occurred
	 * @throws ServletException
	 *             if {@link ServletException} has occurred
	 */
	protected void doRedirect(HttpServletRequest request,
			HttpServletResponse response, Action action, String path)
			throws IOException, ServletException {
		if (path.startsWith("/")) {
			path = request.getContextPath() + path;
		}
		response.sendRedirect(response.encodeRedirectURL(path));
	}

	/**
	 * Do a forward to the path.
	 *
	 * @param request
	 *            the request
	 * @param response
	 *            the response
	 * @param action
	 *            the action
	 * @param path
	 *            the path
	 * @throws IOException
	 *             if {@link IOException} has occurred
	 * @throws ServletException
	 *             if {@link ServletException} has occurred
	 */
	protected void doForward(HttpServletRequest request,
			HttpServletResponse response, Action action, String path)
			throws IOException, ServletException {
		if (!path.startsWith("/")) {
			path = action.basePath + path;
		}
		RequestDispatcher rd = servletContext.getRequestDispatcher(path);
		if (rd == null) {
			response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
					"The request dispatcher specified by the path(" + path
							+ ") is not found.");
			return;
		}
		rd.forward(request, response);
	}

	// =========================================================================================================
	// 下請けメソッド
	// =========================================================================================================

	/**
	 * Capitalizes the text according to JavaBeans specification.
	 *
	 * @param text
	 *            the text
	 *
	 * @return the capitalized text
	 */
	private String capitalize(String text) {
		if (StringUtils.isEmpty(text)) {
			return text;
		}
		char chars[] = text.toCharArray();
		chars[0] = Character.toUpperCase(chars[0]);
		return new String(chars);
	}

	/**
	 * Creates a new instance.
	 *
	 * @param <T>
	 *            the target type
	 * @param clazz
	 *            the class
	 * @return a new instance
	 * @throws WrapRuntimeException
	 *             if an error occurred while creating a new instance.
	 */
	@SuppressWarnings("unchecked")
	private <T> T newInstance(Class<?> clazz) {
		try {
			return (T) clazz.newInstance();
		} catch (Throwable cause) {
			throw new SusanoRuntimeException(
					"An error occurred while creating a new instance of the class("
							+ clazz.getName() + "). Error message: "
							+ cause.getMessage(), cause);
		}
	}

}